Skip to content

Enable opt-in R8 JNI name obfuscation for CoreCLR and NativeAOT - #12575

Draft
simonrozsival wants to merge 14 commits into
mainfrom
simonrozsival-prototype-r8-jni-remapping
Draft

Enable opt-in R8 JNI name obfuscation for CoreCLR and NativeAOT#12575
simonrozsival wants to merge 14 commits into
mainfrom
simonrozsival-prototype-r8-jni-remapping

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Aug 28, 2026

Copy link
Copy Markdown
Member

Context

Advances #12535
Related to dotnet/maui#23027
Related to dotnet/docs-maui#2395

.NET for Android currently disables R8 name obfuscation because managed bindings embed Java/JNI class, method, field, and descriptor names. If R8 renames the Java side while managed metadata and IL retain the original names, JNI lookup and native registration fail.

This change implements build-time rewriting without a runtime lookup table. R8 remains the naming authority: a naming-only pass establishes names, managed assemblies are rewritten before ILLink or NativeAOT ILC, and final R8 performs normal shrinking and optimization with -applymapping.

The feature is opt-in through AndroidEnableR8JniNameObfuscation. It requires AndroidLinkTool=r8, AndroidTypeMapImplementation=trimmable, and either CoreCLR or NativeAOT.

Pipeline

  1. Generate the pre-trim trimmable typemap and Java callable wrappers.
  2. Compile the seed Java graph and run naming-only R8.
  3. Rewrite managed JNI metadata, IL strings, descriptors, typemap attributes, aliases, and FieldRVA data using the seed mapping.
  4. Feed collision-safe, path-hashed rewritten assemblies to ILLink or NativeAOT ILC.
  5. Generate backend-specific final keep rules and a post-link reachability manifest.
  6. Run final R8 with normal shrinking/optimization and -applymapping.
  7. Validate seed/final name stability and required Java-symbol reachability.
  8. Package the resulting DEX and optimized managed output normally.

NativeAOT's prebuilt host performs fixed native lookups against mono.android.Runtime, so that small bridge remains deliberately preserved.

Production hardening

  • Rewritten assemblies are staged under source-path hashes, preventing duplicate-filename and ABI collisions while preserving downstream item metadata.
  • Multi-RID builds use isolated per-RID rewrite outputs and manifests; the shared final Java/R8 pass validates against the first RID's equivalent managed manifest.
  • Strong-named linker/compiler inputs retain their public key and assembly identity plus re-signable signature-directory space, while the stale StrongNameSigned flag is cleared. These private rewritten inputs are intentionally delay-signed because the original private keys are unavailable.
  • NativeAOT multi-RID inner builds retain their normal parallelism.
  • Generated ACW keep rules deduplicate repeated map values; one measured seed configuration decreased from 21,875 rules to 7,271.
  • Seed compilation now runs after intermediate-directory cleanup/setup so clean inner builds have valid stamp directories.

Validation coverage

Automated Release integration tests cover CoreCLR and NativeAOT and verify:

  • clean, no-op, missing-output recovery, and JNI-name invalidation builds;
  • arm64 plus x64 multi-ABI builds with isolated rewritten inputs and manifests;
  • referenced Android libraries containing registered Java peers;
  • custom ProGuard configuration changes invalidating the seed pass, managed rewrite, and final R8;
  • collision-safe hashed staging paths;
  • seed/final mappings plus rewrite and reachability manifests;
  • preservation of public-key assembly identity and delay-signature space.

The focused hardening matrix passes all 7 CoreCLR, NativeAOT, multi-ABI, project-reference/custom-rule, incrementality, and strong-name tests. Existing JNI mapping, rewriting, scanning, mapping-consistency, and reachability coverage remains in place.

Measurements

Metric Baseline Two-pass Delta
CoreCLR signed APK 17,542,007 bytes 17,251,191 bytes -290,816 bytes (-1.66%)
NativeAOT signed APK 18,348,247 bytes 18,016,471 bytes -331,776 bytes (-1.81%)

Across 86 generated typemap assemblies, rewriting 13,031 real typemap entries reduced raw PE size by 1,016,832 bytes (9.42%) and gzip-equivalent size by 107,446 bytes (6.61%). NativeAOT's packaged saving came primarily from classes.dex, because those managed assemblies are compiled into the application native library.

Naming-only R8 remains the dominant added clean-build cost. Replacing it with a managed allocator would reproduce R8 inheritance, interface dispatch, override, keep-rule, and minifier semantics, so this design intentionally keeps R8 as the naming authority.

Remaining follow-up

Additional ACW class-name obfuscation is intentionally deferred. Allowing those classes to be renamed caused CoreCLR final R8 to remove reachable generated native declarations such as nctor_0; the reachability validator correctly rejected the output. Supporting this safely requires a separate native registration/retention design rather than weaker validation or another superficial ProGuard modifier.

simonrozsival and others added 2 commits August 28, 2026 16:07
Rebuild managed PE metadata and IL with obfuscated JNI class, method, field, descriptor, RegisterNatives, and FieldRVA string data. Preserve compression descriptor ordering when rewritten assembly sizes change.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Run a naming-only R8 pass before managed optimization, stage rewritten inputs for ILLink and ILC, and reuse the mapping during final Java shrinking.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival changed the title [prototype] Rewrite managed JNI names after R8 obfuscation [prototype] Two-pass R8 JNI name rewriting before ILLink and ILC Aug 31, 2026
simonrozsival and others added 4 commits August 31, 2026 11:51
Use descriptor-aware reverse member mappings and regenerate the post-link ACW reachability map while retaining the original Java input set.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Record the exact seed mapping entries consumed while rewriting managed assemblies and verify that final R8 preserves every surviving name. Removed entries remain valid linker or R8 trimming outcomes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival changed the title [prototype] Two-pass R8 JNI name rewriting before ILLink and ILC [prototype] Enable R8 JNI name obfuscation for CoreCLR and NativeAOT Aug 31, 2026
simonrozsival and others added 5 commits August 31, 2026 14:38
Generate post-link manifests from the CoreCLR and NativeAOT keep-rule inputs, then fail final R8 when a required Java class or member disappears. Cover registered fields and overload-aware methods in the CoreCLR rules.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Reuse the full JNI rewrite planner in reverse over linked CoreCLR assemblies so final R8 keep rules cover attributes, direct lookups, descriptors, interfaces, typemap metadata, and FieldRVA data. Preserve every ambiguous reverse member candidate and index exact lookups.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Normalize expected multiline output to the host line ending so the new test passes on Windows as well as Unix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add a supported opt-in, collision-safe staging, multi-ABI and project-reference coverage, identity-preserving strong-name handling, and deduplicated ACW keep rules.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival changed the title [prototype] Enable R8 JNI name obfuscation for CoreCLR and NativeAOT Enable opt-in R8 JNI name obfuscation for CoreCLR and NativeAOT Sep 1, 2026
simonrozsival and others added 2 commits September 1, 2026 12:07
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Android PR Reviewer failed. Please review the logs for details.

Generated by Android PR Reviewer for #12575

Restrict reverse mapping during post-link scanning to exact JNI entries recorded by the managed rewrite pass. This prevents ordinary short string literals from being mistaken for R8-obfuscated synthetic classes while preserving strict final reachability validation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Sep 2, 2026
## Summary

Related to #12535

This is layer 1 of 6 in the replacement stack for PR #12575. It extracts the self-contained R8/JNI name-mapping primitives and their focused unit tests so later layers can add build integration and runtime-specific behavior independently.

This layer intentionally changes no build behavior. Nothing invokes these new helpers yet.

## What this adds

- `R8Mapping`
  - Parses R8/ProGuard mapping files, including class, field, method, constructor, overload, line-range, inline-frame, and class-merging entries.
  - Reports the actual source mapping path and line number when file parsing fails.
  - Buffers positional records by residual range and obfuscated name as R8 retrace stacks, indexing only the final residual frame as callable so same-class inline-only methods are not rewritten.
  - Preserves every original class candidate when R8 merges several classes into one residual class.
  - Uses rewrite-manifest class entries to disambiguate reverse class and member mappings, failing closed unless exactly one original class remains allowed.
  - Fails closed when reverse field or name-only method lookup remains ambiguous, while allowing a manifest filter to select one candidate safely.
  - Records the owning class together with every successful forward member lookup, allowing the resulting manifest to restrict its own reverse mapping safely.
  - Tracks accessed entries atomically across concurrent lookups and exposes stable snapshots to manifest consumers.
  - Reports a removed declaring class even when reachability input starts with only a required field or method entry, without emitting duplicate class conflicts.
  - Provides compatibility and reachability conflict detection used by later stack layers.
  - Produces and consumes compact, sorted manifest entries with deterministic `\n` newlines.
- `JniDescriptorText`
  - Validates JNI field and method descriptors, including non-empty slash-separated object type names.
  - Rejects malformed object descriptors containing Java source-name dots, array markers inside class names, or empty path segments.
  - Rewrites embedded object type names while preserving primitive and unrelated types.
  - Converts validated JNI descriptor tokens into Java source type names without redundantly rescanning tokens parsed from a method descriptor.
  - Preserves nested binary-name `$` separators when converting descriptors to R8 mapping keys.
- `LdstrRewriter`
  - Rewrites JNI-sensitive string constants using an `IJniNameMapping` implementation.
  - Handles encoded method and field IDs, constructor descriptors, `RegisterNatives` entries, multiline registration blocks, and exact JNI class-name strings.
  - Leaves unrelated or unresolved strings unchanged.

## Test coverage

Adds focused tests for:

- R8 mapping syntax, source-aware parse errors, positional inline stacks, qualified and same-class inline frames, residual method preservation, merged-class reverse lookup, manifest disambiguation, zero/multiple-candidate failure, concurrent access tracking and snapshot semantics, member-only owning-class tracking, forward and reverse lookups, overloads, constructors, nested classes, deterministic manifest output, and conflict detection.
- JNI method/field descriptor validation, malformed object descriptors, object and array rewriting, Java type conversion, nested `$` descriptor-to-mapping-key conversion, and malformed standalone token handling.
- JNI-related `ldstr` forms, including encoded member IDs, constructors, registration blocks, class names, unchanged inputs, and a forward/reverse field round trip restricted by the forward access manifest.

Validation commands:

```sh
./dotnet-local.sh test src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj -v minimal --filter 'FullyQualifiedName~R8MappingTests|FullyQualifiedName~JniDescriptorTextTests|FullyQualifiedName~LdstrRewriterTests'
./dotnet-local.sh build src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj --no-restore -v minimal
```

Result: **82 passed, 0 failed, 0 skipped**; test project build succeeded with **0 warnings and 0 errors**.

## Intentionally out of scope

The following remain for later layers in the replacement stack:

- Build integration and MSBuild tasks/targets.
- Metadata assembly rebuilding.
- Typemap `FieldRVA` handling.
- CoreCLR integration.
- NativeAOT integration.
- User-facing documentation.

The initial implementation and tests in this layer were reconstructed from the verified PR #12575 source snapshot at `ac2b30593ff5afa0659fe35d3de515239c7540fe`, followed by descriptor-validation, ambiguity-safety, diagnostics, concurrency, merged-class, inline-frame, and owning-class access-tracking hardening from review. PR #12575 and its branch remain unchanged.
simonrozsival added a commit that referenced this pull request Sep 3, 2026
## Summary

This is layer 2 of 6 in the replacement stack for PR #12575. It adds the internal PE and metadata reconstruction substrate required by later JNI name-rewriting layers, without enabling any build behavior.

- Rebuilds managed PE images while preserving existing metadata tokens and supported table row ordering.
- Re-emits method bodies, embedded resources, debug-directory data, native resources, and `FieldRVA` mapped data, including ranges from different PE sections.
- Preserves overlapping/aliased `FieldRVA` backing data by relocating rewritten fields instead of modifying shared storage.
- Validates malformed IL, resource offsets/directories, and strong-name signature directories through controlled rewrite errors.
- Emits event/property method semantics explicitly in `HasSemantics` coded-index order.
- Rejects unsupported field-backed `ImplMap` rows instead of silently dropping metadata.
- Adds low-level metadata encoding, raw-column, IL operand-scanning, and rewrite-plan primitives.
- Covers longer and shorter UTF-8 `FieldRVA` replacements, aliased mapped fields, event/property accessor preservation, per-use `#US` splitting, and strong-name signature preservation directly through `AssemblyRebuilder`.
- Uses the executing runtime's core assembly version in generated fixtures so rebuilt images load across target frameworks.

## Scope

This layer intentionally does **not** add JNI-specific planning or attribute/`ldstr` orchestration, the assembly-rewriter facade, the `RewriteJniNamesForR8` MSBuild task, typemap rewrite behavior, CoreCLR/NativeAOT target integration, or documentation. Those surfaces belong to later stack layers.

## Validation

- `./dotnet-local.sh build src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj -c Debug -v:minimal --no-restore`
- `./dotnet-local.sh test bin/TestDebug/net10.0/Xamarin.Android.Build.Tests.dll --filter 'FullyQualifiedName~NativeResourceSectionCopierTests|FullyQualifiedName~AssemblyRebuilderTests'`
- Direct layer-2 result: 18 passed, 0 failed.
- Cumulative layer-3 `RewrittenAssemblyLoadsAndRunsInTheRuntime` result: 1 passed, 0 failed.

Related to #12535

Depends on #12628
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant